# -*- coding: utf-8 -*-
# =============================================================================
#     FileName:
#         Desc:
#       Author: GGA
#        Email:
#     HomePage:
#      Version: 1.0.1
#   LastChange: 2020-12-20
#      History:
# =============================================================================
import json
import os
import sys
import subprocess
import logging
import re
import hashlib
import datetime

import fire

from utils.logger_helper import LoggerHelper
from common_config import CommonConfig
from mysql_binlog_parser import MySQLBinlogParser

logger = LoggerHelper.get_logger()


class MySQLRowDataChecker(object):

    @classmethod
    def get_md5(cls, source_text: str):
        """
        计算文本的MD5值
        :param source_text:
        :return:
        """
        return hashlib.md5(source_text.encode(encoding='UTF-8')).hexdigest()

    @classmethod
    def get_check_result_file(cls):
        log_path = os.path.join(os.path.curdir, "logs")
        if not os.path.exists(log_path):
            os.makedirs(log_path)
        dt_str = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
        return os.path.join(log_path, "check_result_{}.log".format(dt_str))

    @classmethod
    def write_check_result(cls, check_result_file, file_content):
        with open(file=check_result_file, encoding="utf-8", mode="a+") as fw:
            fw.write(file_content + "\n")

    @classmethod
    def get_check_column_value(cls, column_value, use_json, use_md5):
        if column_value is None:
            return "NONE"
        if use_json:
            column_value = json.dumps(json.load(column_value), sort_keys=True)
        if use_md5:
            column_value = cls.get_md5(column_value)
        return str(column_value)

    @classmethod
    def get_full_value(cls, image_items, check_items):
        full_value = ""
        for check_key in check_items:
            column_index = check_key["column_index"]
            use_md5 = check_key["use_md5"]
            use_json = check_key["use_json"]
            column_value = cls.get_check_column_value(
                column_value=image_items.get(str(column_index), None),
                use_md5=use_md5, use_json=use_json
            )
            full_value += ":::" + column_value
        return full_value

    @classmethod
    def parse_dml_event(cls, table_action, action_time, before_image_items, after_image_items, check_key_items,
                        check_value_items, check_result_file):
        before_full_key = cls.get_full_value(image_items=before_image_items, check_items=check_key_items)
        after_full_key = cls.get_full_value(image_items=after_image_items, check_items=check_key_items)
        before_full_value = cls.get_full_value(image_items=before_image_items, check_items=check_value_items)
        after_full_value = cls.get_full_value(image_items=after_image_items, check_items=check_value_items)
        check_result = None
        if table_action == "INSERT":
            check_result = (
                "key_md5: {}, value_md5: {}, type: {}, op_time: {}, key: {}, before_value: {}").format(
                cls.get_md5(after_full_key), cls.get_md5(after_full_value),
                table_action, action_time, after_full_key, after_full_value
            )
        if table_action == "DELETE":
            check_result = (
                "key_md5: {}, value_md5: {}, type: {}, op_time: {}, key: {}, before_value: {}").format(
                cls.get_md5(before_full_key), cls.get_md5(before_full_value),
                table_action, action_time, before_full_key, before_full_value
            )
        if table_action == "UPDATE":
            check_result = (
                "key_md5: {}, value_md5: {}, type: {}, op_time: {}, after_key: {}, before_key: {}, after_value: {}, before_value:{}").format(
                cls.get_md5(after_full_key), cls.get_md5(after_full_value),
                table_action, action_time, after_full_key, before_full_key, after_full_value, before_full_value
            )
        if check_result is not None:
            cls.write_check_result(check_result_file=check_result_file, file_content=check_result)

    @classmethod
    def get_check_items(cls, check_indexes):
        check_items = list()
        for check_index in check_indexes.split(sep=","):
            check_index = check_index.strip()
            if check_index == "" or check_index.find("@") < 0:
                continue
            matches = re.findall(r"@(\d+)", check_index)
            if not matches:
                continue
            column_index = matches[0]
            check_item = {"column_index": column_index, "use_md5": False, "use_json": False}
            if check_index.find("md5") >= 0:
                check_item["use_md5"] = True
            if check_index.find("json") >= 0:
                check_item["use_json"] = True
            check_items.append(check_item)
        return check_items

    @classmethod
    def check_binlog(cls, binlog_path, database_name, table_name, key_indexes, value_indexes):
        if not os.path.exists(binlog_path):
            logger.warning("指定的binlog存在")
            return False
        check_key_items = cls.get_check_items(key_indexes)
        check_value_items = cls.get_check_items(value_indexes)
        check_result_file = cls.get_check_result_file()
        logger.info("开始解析binlog: {}".format(binlog_path))
        cmd_temp = """ {mysqlbinlog_exe} -v --base64-output=DECODE-ROWS '{binlog_path}' """
        cmd = cmd_temp.format(
            mysqlbinlog_exe=CommonConfig.MYSQLBINLOG_EXE,
            binlog_path=binlog_path
        )
        parse_process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
        current_line_num = 0
        is_target_table = False
        insert_flag = "### INSERT INTO `{}`.`{}`".format(database_name, table_name)
        update_flag = "### UPDATE `{}`.`{}`".format(database_name, table_name)
        delete_flag = "### DELETE FROM `{}`.`{}`".format(database_name, table_name)
        is_after_image = False
        is_before_image = False
        before_image_items = []
        after_image_items = []
        table_action = None
        action_time = None
        while True:
            file_bytes = parse_process.stdout.readline()
            if not file_bytes:
                break
            current_line_num += 1
            if current_line_num % 10000 == 0:
                logger.info("当前处理到第{}行".format(current_line_num))
            current_line = file_bytes.decode().strip()
            if current_line.startswith("# at "):
                if is_target_table:
                    # TODO: 处理上一个事件
                    cls.parse_dml_event(
                        table_action=table_action,
                        action_time=action_time,
                        before_image_items=before_image_items,
                        after_image_items=after_image_items,
                        check_key_items=check_key_items,
                        check_value_items=check_value_items,
                        check_result_file=check_result_file
                    )
                is_target_table = False
                continue
            if current_line.find("server id") > 0:
                action_time = MySQLBinlogParser.parse_binlog_datetime(current_line)
            if current_line.startswith(insert_flag):
                is_target_table = True
                table_action = "INSERT"
                is_after_image = False
                is_before_image = False
                before_image_items = dict()
                after_image_items = dict()
                continue
            if current_line.startswith(delete_flag):
                is_target_table = True
                table_action = "DELETE"
                is_after_image = False
                is_before_image = False
                before_image_items = dict()
                after_image_items = dict()
                continue
            if current_line.startswith(update_flag):
                is_target_table = True
                table_action = "UPDATE"
                is_after_image = False
                is_before_image = False
                before_image_items = dict()
                after_image_items = dict()
                continue
            if current_line.startswith("### SET"):
                is_after_image = True
                continue
            if current_line.startswith("### WHERE"):
                is_before_image = True
                continue
            if current_line.startswith("###   @"):
                if not is_target_table:
                    continue
                column_index, column_value = MySQLBinlogParser.parse_table_column_line(current_line)
                if is_after_image:
                    after_image_items[str(column_index)] = column_value
                if is_before_image:
                    before_image_items[str(column_index)] = column_value
                continue
            # end while
        logger.info("解析binlog完成")
        logger.info("解析结果保存在：{}".format(check_result_file))


def check_row_data(binlog_path, database_name, table_name, key_indexes, value_indexes):
    """

    :param binlog_path:
    :param database_name:
    :param table_name:
    :param key_indexes:
    :param value_indexes:
    :return:
    """
    LoggerHelper.init_logger()
    MySQLRowDataChecker.check_binlog(
        binlog_path=binlog_path,
        database_name=database_name,
        table_name=table_name,
        key_indexes=key_indexes, value_indexes=value_indexes
    )


if __name__ == '__main__':
    fire.Fire(check_row_data)
